# -*- coding: UTF-8 -*-
from django.core import formfields, template_loader, validators
from django.core.mail import mail_admins, mail_managers
from django.core.exceptions import Http404, ObjectDoesNotExist
from django.core.extensions import DjangoContext as Context
from django.models.auth import sessions
from django.models.comments import comments, freecomments
from django.models.core import contenttypes
from django.parts.auth.formfields import AuthenticationForm
from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
from django.utils.text import normalize_newlines
from django.conf.settings import BANNED_IPS, COMMENTS_ALLOW_PROFANITIES, COMMENTS_SKETCHY_USERS_GROUP, COMMENTS_FIRST_FEW, SITE_ID
import base64, datetime

COMMENTS_PER_PAGE = 20

class PublicCommentManipulator(AuthenticationForm):
    "Un manipulateur qui gère les commentaires publics créés par des utilisateurs inscrits"
    def __init__(self, user, ratings_required, ratings_range, num_rating_choices):
        AuthenticationForm.__init__(self)
        self.ratings_range, self.num_rating_choices = ratings_range, num_rating_choices
        choices = [(c, c) for c in ratings_range]
        def get_validator_list(rating_num):
            if rating_num <= num_rating_choices:
                return [validators.RequiredIfOtherFieldsGiven(['rating%d' % i for i in range(1, 9) if i != rating_num], "L'entrée de cette note est obligatoire car vous avez entré au moins une autre note.")]
            else:
                return []
        self.fields.extend([
            formfields.LargeTextField(field_name="comment", maxlength=3000, is_required=True,
                validator_list=[self.hasNoProfanities]),
            formfields.RadioSelectField(field_name="rating1", choices=choices,
                is_required=ratings_required and num_rating_choices > 0,
                validator_list=get_validator_list(1),
            ),
            formfields.RadioSelectField(field_name="rating2", choices=choices,
                is_required=ratings_required and num_rating_choices > 1,
                validator_list=get_validator_list(2),
            ),
            formfields.RadioSelectField(field_name="rating3", choices=choices,
                is_required=ratings_required and num_rating_choices > 2,
                validator_list=get_validator_list(3),
            ),
            formfields.RadioSelectField(field_name="rating4", choices=choices,
                is_required=ratings_required and num_rating_choices > 3,
                validator_list=get_validator_list(4),
            ),
            formfields.RadioSelectField(field_name="rating5", choices=choices,
                is_required=ratings_required and num_rating_choices > 4,
                validator_list=get_validator_list(5),
            ),
            formfields.RadioSelectField(field_name="rating6", choices=choices,
                is_required=ratings_required and num_rating_choices > 5,
                validator_list=get_validator_list(6),
            ),
            formfields.RadioSelectField(field_name="rating7", choices=choices,
                is_required=ratings_required and num_rating_choices > 6,
                validator_list=get_validator_list(7),
            ),
            formfields.RadioSelectField(field_name="rating8", choices=choices,
                is_required=ratings_required and num_rating_choices > 7,
                validator_list=get_validator_list(8),
            ),
        ])
        if not user.is_anonymous():
            self["username"].is_required = False
            self["username"].validator_list = []
            self["password"].is_required = False
            self["password"].validator_list = []
            self.user_cache = user

    def hasNoProfanities(self, field_data, all_data):
        if COMMENTS_ALLOW_PROFANITIES:
            return
        return validators.hasNoProfanities(field_data, all_data)

    def get_comment(self, new_data):
        "Fonction assistante"
        return comments.Comment(None, self.get_user_id(), new_data["content_type_id"],
            new_data["object_id"], new_data.get("headline", "").strip(),
            new_data["comment"].strip(), new_data.get("rating1", None),
            new_data.get("rating2", None), new_data.get("rating3", None),
            new_data.get("rating4", None), new_data.get("rating5", None),
            new_data.get("rating6", None), new_data.get("rating7", None),
            new_data.get("rating8", None), new_data.get("rating1", None) is not None,
            datetime.datetime.now(), new_data["is_public"], new_data["ip_address"], False, SITE_ID)

    def save(self, new_data):
        today = datetime.date.today()
        c = self.get_comment(new_data)
        for old in comments.get_list(content_type_id__exact=new_data["content_type_id"],
            object_id__exact=new_data["object_id"], user_id__exact=self.get_user_id()):
            # On vérifie si le commentaire actuel n'est pas un doublon. (Il arrive
            # que des gens postent par erreur un même commentaire deux fois de
            # suite.) Si c'est le cas, on échoue de manière silencieuse en prétendant
            # que le commentaire a bien été posté.
            if old.submit_date.date() == today and old.comment == c.comment \
                and old.rating1 == c.rating1 and old.rating2 == c.rating2 \
                and old.rating3 == c.rating3 and old.rating4 == c.rating4 \
                and old.rating5 == c.rating5 and old.rating6 == c.rating6 \
                and old.rating7 == c.rating7 and old.rating8 == c.rating8:
                return old
            # Si l'utilisateur a laissé une note, on invalide toutes les anciennes notes.
            if c.rating1 is not None:
                old.valid_rating = False
                old.save()
        c.save()
        # Si la personne postant le commentaire a posté moins de COMMENTS_FIRST_FEW
        # commentaires, on envoie le commentaire aux gérants du site.
        if self.user_cache.get_comments_comment_count() <= COMMENTS_FIRST_FEW:
            message = 'Le commentaire suivant a été posté par un utilisateur qui a posté moins de %s commentaire(s) :\n\n%s' % \
                (COMMENTS_FIRST_FEW, c.get_as_text())
            mail_managers("Commentaire posté par un nouvel utilisateur", message)
        if COMMENTS_SKETCHY_USERS_GROUP and COMMENTS_SKETCHY_USERS_GROUP in [g.id for g in self.user_cache.get_group_list()]:
            message = 'Le commentaire suivant a été posté par un utilisateur louche :\n\n%s' % c.get_as_text()
            mail_managers("Commentaire posté par un utilisateur louche (%s)" % self.user_cache.username, c.get_as_text())
        return c

class PublicFreeCommentManipulator(formfields.Manipulator):
    "Un manipulateur qui gère les commentaires publics créés par des utilisateurs non-inscrits (commentaires libres)"
    def __init__(self):
        self.fields = (
            formfields.TextField(field_name="person_name", maxlength=50, is_required=True,
                validator_list=[self.hasNoProfanities]),
            formfields.LargeTextField(field_name="comment", maxlength=3000, is_required=True,
                validator_list=[self.hasNoProfanities]),
        )

    def hasNoProfanities(self, field_data, all_data):
        if COMMENTS_ALLOW_PROFANITIES:
            return
        return validators.hasNoProfanities(field_data, all_data)

    def get_comment(self, new_data):
        "Fonction assistante"
        return freecomments.FreeComment(None, new_data["content_type_id"],
            new_data["object_id"], new_data["comment"].strip(),
            new_data["person_name"].strip(), datetime.datetime.now(), new_data["is_public"],
            new_data["ip_address"], False, SITE_ID)

    def save(self, new_data):
        today = datetime.date.today()
        c = self.get_comment(new_data)
        # On vérifie si le commentaire actuel n'est pas un doublon. (Il arrive
        # que des gens postent par erreur un même commentaire deux fois de
        # suite.) Si c'est le cas, on échoue de manière silencieuse en prétendant
        # que le commentaire a bien été posté.
        for old_comment in freecomments.get_list(content_type_id__exact=new_data["content_type_id"],
            object_id__exact=new_data["object_id"], person_name__exact=new_data["person_name"],
            submit_date__year=today.year, submit_date__month=today.month,
            submit_date__day=today.day):
            if old_comment.comment == c.comment:
                return old_comment
        c.save()
        return c

def post_comment(request):
    """
    Poste un commentaire

    En cas de succès, redirige l'utilisateur vers la vue `comments.comments.comment_was_posted`.

    Modèles : `comment_preview`
    Contexte :
        comment
            le commentaire qui est posté
        comment_form
            le formulaire permettant d'ajouter un commentaire
        options
            les options du commentaire
        target
            la cible du commentaire
        hash
            le hash de sécurité (pour qu'un commentaire soit effectivement posté, ce hash doit
            accompagner les autres données du formulaire)
        rating_options
            les options du commentaire relatives aux notes
        ratings_optional
            les notes sont-elles facultatives?
        ratings_required
            les notes sont-elles obligatoires?
        rating_range
            la gamme de notes possibles
        rating_choices
            les notes possibles
    """
    if not request.POST:
        raise Http404, "Seul l'envoi de données par la méthode POST est autorisé"
    try:
        options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo']
    except KeyError:
        raise Http404, "Un ou plusieurs champs obligatoires n'ont pas été envoyé"
    photo_options = request.POST.get('photo_options', '')
    rating_options = normalize_newlines(request.POST.get('rating_options', ''))
    if comments.get_security_hash(options, photo_options, rating_options, target) != security_hash:
        raise Http404, "Quelqu'un a malicieusement altéré les données du formulaire (violation de sécurité)"
    # Maintenant nous sommes assurés de la validité des données.
    if rating_options:
        rating_range, rating_choices = comments.get_rating_options(base64.decodestring(rating_options))
    else:
        rating_range, rating_choices = [], []
    content_type_id, object_id = target.split(':') # la variable target est du style '52:5157'
    try:
        obj = contenttypes.get_object(id__exact=content_type_id).get_object_for_this_type(id__exact=object_id)
    except ObjectDoesNotExist:
        raise Http404, "Le paramètre 'target' du formulaire permettant d'envoyer un commentaire est invalide (plus précisément, c'est l'identifiant de l'objet qui est invalide)"
    option_list = options.split(',') # la variable options est du style 'pa,ra'
    new_data = request.POST.copy()
    new_data['content_type_id'] = content_type_id
    new_data['object_id'] = object_id
    new_data['ip_address'] = request.META['REMOTE_ADDR']
    new_data['is_public'] = comments.IS_PUBLIC in option_list
    response = HttpResponse()
    manipulator = PublicCommentManipulator(request.user,
        ratings_required=comments.RATINGS_REQUIRED in option_list,
        ratings_range=rating_range,
        num_rating_choices=len(rating_choices))
    errors = manipulator.get_validation_errors(new_data)
    # Si l'utilisateur a fourni une paire nom d'utilisateur/mot de passe correcte et s'il n'était
    # pas déjà connecté, on le connecte pour qu'il n'ait pas à entrer de nouveau son nom d'utilisateur
    # et son mot de passe.
    if manipulator.get_user() and new_data.has_key('password') and manipulator.get_user().check_password(new_data['password']):
        sessions.start_web_session(manipulator.get_user_id(), request, response)
    if errors or request.POST.has_key('preview'):
        class CommentFormWrapper(formfields.FormWrapper):
            def __init__(self, manipulator, new_data, errors, rating_choices):
                formfields.FormWrapper.__init__(self, manipulator, new_data, errors)
                self.rating_choices = rating_choices
            def ratings(self):
                field_list = [self['rating%d' % (i+1)] for i in range(len(rating_choices))]
                for i, f in enumerate(field_list):
                    f.choice = rating_choices[i]
                return field_list
        comment = errors and '' or manipulator.get_comment(new_data)
        comment_form = CommentFormWrapper(manipulator, new_data, errors, rating_choices)
        t = template_loader.get_template('comments/preview')
        c = Context(request, {
            'comment': comment,
            'comment_form': comment_form,
            'options': options,
            'target': target,
            'hash': security_hash,
            'rating_options': rating_options,
            'ratings_optional': comments.RATINGS_OPTIONAL in option_list,
            'ratings_required': comments.RATINGS_REQUIRED in option_list,
            'rating_range': rating_range,
            'rating_choices': rating_choices,
        })
    elif request.POST.has_key('post'):
        # Si l'adresse IP de l'utilisateur fait partie des adresses bannies, alors
        # on envoie un mail aux administrateurs, on N'ENREGISTRE PAS le commentaire et
        # on sert la page "Merci d'avoir posté votre commentaire" comme si de rien n'était.
        if request.META['REMOTE_ADDR'] in BANNED_IPS:
            mail_admins("Un utilisateur banni a essayé de poster un commentaire", str(request.POST) + "\n\n" + str(request.META))
        else:
            manipulator.do_html2python(new_data)
            comment = manipulator.save(new_data)
        return HttpResponseRedirect("/comments/posted/?c=%s:%s" % (content_type_id, object_id))
    else:
        raise Http404, "Le formulaire permettant d'envoyer un commentaire n'a fourni ni l'action 'preview' ni l'action 'post'"
    response.write(t.render(c))
    return response

def post_free_comment(request):
    """
    Poste un commentaire libre (un commentaire qui ne nécessite pas d'être connecté)

    En cas de succès, redirige l'utilisateur vers la vue `comments.comments.comment_was_posted`.

    Modèles : `comment_free_preview`
    Contexte :
        comment
            le commentaire qui est posté
        comment_form
            le formulaire permettant d'ajouter un commentaire
        options
            les options du commentaire
        target
            la cible du commentaire
        hash
            le hash de sécurité (pour qu'un commentaire soit effectivement posté, ce hash doit
            accompagner les autres données du formulaire)
    """
    if not request.POST:
        raise Http404, "Seul l'envoi de données par la méthode POST est autorisé"
    try:
        options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo']
    except KeyError:
        raise Http404, "Un ou plusieurs champs obligatoires n'ont pas été envoyé"
    if comments.get_security_hash(options, '', '', target) != security_hash:
        raise Http404, "Quelqu'un a malicieusement altéré les données du formulaire (violation de sécurité)"
    content_type_id, object_id = target.split(':') # la variable target est du style '52:5157'
    content_type = contenttypes.get_object(id__exact=content_type_id)
    try:
        obj = content_type.get_object_for_this_type(id__exact=object_id)
    except ObjectDoesNotExist:
        raise Http404, "Le paramètre 'target' du formulaire permettant d'envoyer un commentaire est invalide (plus précisément, c'est l'identifiant de l'objet qui est invalide)"
    option_list = options.split(',')
    new_data = request.POST.copy()
    new_data['content_type_id'] = content_type_id
    new_data['object_id'] = object_id
    new_data['ip_address'] = request.META['REMOTE_ADDR']
    new_data['is_public'] = comments.IS_PUBLIC in option_list
    response = HttpResponse()
    manipulator = PublicFreeCommentManipulator()
    errors = manipulator.get_validation_errors(new_data)
    if errors or request.POST.has_key('preview'):
        comment = errors and '' or manipulator.get_comment(new_data)
        t = template_loader.get_template('comments/free_preview')
        c = Context(request, {
            'comment': comment,
            'comment_form': formfields.FormWrapper(manipulator, new_data, errors),
            'options': options,
            'target': target,
            'hash': security_hash,
        })
    elif request.POST.has_key('post'):
        # Si l'adresse IP de l'utilisateur fait partie des adresses bannies, alors
        # on envoie un mail aux administrateurs, on N'ENREGISTRE PAS le commentaire et
        # on sert la page "Merci d'avoir posté votre commentaire" comme si de rien n'était.
        if request.META['REMOTE_ADDR'] in BANNED_IPS:
            from django.core.mail import mail_admins
            mail_admins("Farceur", str(request.POST) + "\n\n" + str(request.META))
        else:
            manipulator.do_html2python(new_data)
            comment = manipulator.save(new_data)
        return HttpResponseRedirect("/comments/posted/?c=%s:%s" % (content_type_id, object_id))
    else:
        raise Http404, "Le formulaire permettant d'envoyer un commentaire n'a fourni ni l'action 'preview' ni l'action 'post'"
    response.write(t.render(c))
    return response

def comment_was_posted(request):
    """
    Affiche une page du style "le commentaire a bien été posté"

    Modèles : `comment_posted`
    Contexte :
        object
            L'objet que le commentaire concerne
    """
    obj = None
    if request.GET.has_key('c'):
        content_type_id, object_id = request.GET['c'].split(':')
        try:
            content_type = contenttypes.get_object(id__exact=content_type_id)
            obj = content_type.get_object_for_this_type(id__exact=object_id)
        except ObjectDoesNotExist:
            pass
    t = template_loader.get_template('comments/posted')
    c = Context(request, {
        'object': obj,
    })
    return HttpResponse(t.render(c))
